﻿//******************************************************************************************************
//  AdapterBase.cs - Gbtc
//
//  Copyright © 2012, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  09/02/2010 - J. Ritchie Carroll
//       Generated original version of source code.
//  11/01/2013 - Stephen C. Wills
//       Updated to process time-series entities.
//
//******************************************************************************************************

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GSF.Collections;
using GSF.Configuration;
using GSF.Data;
using GSF.Units;

namespace GSF.TimeSeries.Adapters
{
    /// <summary>
    /// Represents the base class for any adapter.
    /// </summary>
    public abstract class AdapterBase : IAdapter
    {
        #region [ Members ]

        // Constants
        private const int DefaultEntityReportingInterval = 100000;

        /// <summary>
        /// Default initialization timeout.
        /// </summary>
        public const int DefaultInitializationTimeout = 15000;

        // Events

        /// <summary>
        /// Provides status messages to consumer.
        /// </summary>
        /// <remarks>
        /// <see cref="EventArgs{T}.Argument"/> is new status message.
        /// </remarks>
        public event EventHandler<EventArgs<string>> StatusMessage;

        /// <summary>
        /// Event is raised when there is an exception encountered while processing.
        /// </summary>
        /// <remarks>
        /// <see cref="EventArgs{T}.Argument"/> is the exception that was thrown.
        /// </remarks>
        public event EventHandler<EventArgs<Exception>> ProcessException;

        /// <summary>
        /// Event is raised when <see cref="InputSignalIDs"/> are updated.
        /// </summary>
        public event EventHandler InputSignalIDsUpdated;

        /// <summary>
        /// Event is raised when <see cref="OutputSignalIDs"/> are updated.
        /// </summary>
        public event EventHandler OutputSignalIDsUpdated;

        /// <summary>
        /// Event is raised when adapter is aware of a configuration change.
        /// </summary>
        public event EventHandler ConfigurationChanged;

        /// <summary>
        /// This event is raised if there are any time-series entities being discarded during processing.
        /// </summary>
        /// <remarks>
        /// <see cref="EventArgs{T}.Argument"/> is the enumeration of <see cref="ITimeSeriesEntity"/> objects that are being discarded during processing.
        /// </remarks>
        public event EventHandler<EventArgs<IEnumerable<ITimeSeriesEntity>>> EntitiesDiscarded;

        /// <summary>
        /// Event is raised when <see cref="AdapterBase"/> is disposed.
        /// </summary>
        public event EventHandler Disposed;

        // Fields
        private string m_name;
        private uint m_id;
        private string m_connectionString;
        private Dictionary<string, string> m_settings;
        private DataSet m_dataSource;
        private int m_initializationTimeout;
        private bool m_autoStart;
        private readonly ObservableSet<Guid> m_inputSignals;
        private readonly ObservableSet<Guid> m_outputSignals;
        private long m_processedEntities;
        private int m_entityReportingInterval;
        private bool m_enabled;
        private long m_startTime;
        private long m_stopTime;
        private DateTime m_startTimeConstraint;
        private DateTime m_stopTimeConstraint;
        private int m_processingInterval;
        private bool m_initialized;
        private bool m_disposed;

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Constructs a new instance of the <see cref="AdapterBase"/>.
        /// </summary>
        protected AdapterBase()
        {
            m_name = GetType().Name;
            m_settings = new Dictionary<string, string>();

            m_inputSignals = new ObservableSet<Guid>();
            m_outputSignals = new ObservableSet<Guid>();

            m_inputSignals.CollectionChanged += m_inputSignals_CollectionChanged;
            m_outputSignals.CollectionChanged += m_outputSignals_CollectionChanged;

            m_startTimeConstraint = DateTime.MinValue;
            m_stopTimeConstraint = DateTime.MaxValue;
            m_processingInterval = -1;

            m_initializationTimeout = DefaultInitializationTimeout;
        }

        /// <summary>
        /// Releases the unmanaged resources before the <see cref="AdapterBase"/> object is reclaimed by <see cref="GC"/>.
        /// </summary>
        ~AdapterBase()
        {
            Dispose(false);
        }

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets or sets the name of this <see cref="AdapterBase"/>.
        /// </summary>
        /// <remarks>
        /// Derived classes should provide a name for the adapter.
        /// </remarks>
        public virtual string Name
        {
            get
            {
                return m_name;
            }
            set
            {
                m_name = value;
            }
        }

        /// <summary>
        /// Gets or sets numeric ID associated with this <see cref="AdapterBase"/>.
        /// </summary>
        public virtual uint ID
        {
            get
            {
                return m_id;
            }
            set
            {
                m_id = value;
            }
        }

        /// <summary>
        /// Gets or sets key/value pair connection information specific to this <see cref="AdapterBase"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Example connection string using manually defined signals:<br/>
        /// <c>inputSignals={P1:1245;P1:1247;P2:1335};</c><br/>
        /// <c>outputSignals={P3:1345;P3:1346;P3:1347}</c><br/>
        /// When defined manually, elements in key are defined as "ArchiveSource:PointID"<br/>
        /// </para>
        /// <para>
        /// Example connection string using signals defined in a <see cref="DataSource"/> table:<br/>
        /// <c>inputSignals={FILTER ActiveMeasurements WHERE Company='GSF' AND SignalType='FREQ' ORDER BY ID};</c><br/>
        /// <c>outputSignals={FILTER ActiveMeasurements WHERE SignalType IN ('IPHA','VPHA') AND Phase='+'}</c><br/>
        /// <br/>
        /// Basic filtering syntax is as follows:<br/>
        /// <br/>
        ///     {FILTER &lt;TableName&gt; WHERE &lt;Expression&gt; [ORDER BY &lt;SortField&gt;]}<br/>
        /// <br/>
        /// Source tables are expected to have at least the following fields:<br/>
        /// <list type="table">
        ///     <listheader>
        ///         <term>Name</term>
        ///         <term>Type</term>
        ///         <description>Description.</description>
        ///     </listheader>
        ///     <item>
        ///         <term>ID</term>
        ///         <term>NVARCHAR</term>
        ///         <description>Measurement key formatted as: ArchiveSource:PointID.</description>
        ///     </item>
        ///     <item>
        ///         <term>PointTag</term>
        ///         <term>NVARCHAR</term>
        ///         <description>Point tag of signal.</description>
        ///     </item>
        ///     <item>
        ///         <term>Adder</term>
        ///         <term>FLOAT</term>
        ///         <description>Adder to apply to value, if any (default to 0.0).</description>
        ///     </item>
        ///     <item>
        ///         <term>Multiplier</term>
        ///         <term>FLOAT</term>
        ///         <description>Multiplier to apply to value, if any (default to 1.0).</description>
        ///     </item>
        /// </list>
        /// </para>
        /// </remarks>
        public virtual string ConnectionString
        {
            get
            {
                return m_connectionString;
            }
            set
            {
                m_connectionString = value;

                // Preparse settings upon connection string assignment
                if (string.IsNullOrWhiteSpace(m_connectionString))
                    m_settings = new Dictionary<string, string>();
                else
                    m_settings = m_connectionString.ParseKeyValuePairs();
            }
        }

        /// <summary>
        /// Gets or sets <see cref="DataSet"/> based data source available to this <see cref="AdapterBase"/>.
        /// </summary>
        public virtual DataSet DataSource
        {
            get
            {
                return m_dataSource;
            }
            set
            {
                m_dataSource = value;
            }
        }

        /// <summary>
        /// Gets or sets maximum time system will wait during <see cref="Start"/> for initialization.
        /// </summary>
        /// <remarks>
        /// Set to <see cref="Timeout.Infinite"/> to wait indefinitely.
        /// </remarks>
        public virtual int InitializationTimeout
        {
            get
            {
                return m_initializationTimeout;
            }
            set
            {
                m_initializationTimeout = value;
            }
        }

        /// <summary>
        /// Gets or sets flag indicating if adapter should automatically start or otherwise connect on demand.
        /// </summary>
        public virtual bool AutoStart
        {
            get
            {
                return m_autoStart;
            }
            set
            {
                m_autoStart = value;
            }
        }

        /// <summary>
        /// Gets input signals the <see cref="AdapterBase"/> expects, if any.
        /// </summary>
        [ConnectionStringParameter,
        DefaultValue(null),
        Description("Defines primary keys of input signals the adapter expects; can be one of a filter expression, measurement key, point tag or Guid."),
        CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")]
        public virtual ISet<Guid> InputSignalIDs
        {
            get
            {
                return m_inputSignals;
            }
        }

        /// <summary>
        /// Gets output signals that the <see cref="AdapterBase"/> will produce, if any.
        /// </summary>
        [ConnectionStringParameter,
        DefaultValue(null),
        Description("Defines primary keys of output signals the adapter will produce; can be one of a filter expression, measurement key, point tag or Guid."),
        CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")]
        public virtual ISet<Guid> OutputSignalIDs
        {
            get
            {
                return m_outputSignals;
            }
        }

        /// <summary>
        /// Gets the total number of time-series entities handled thus far by the <see cref="AdapterBase"/>.
        /// </summary>
        public virtual long ProcessedEntities
        {
            get
            {
                return m_processedEntities;
            }
        }

        /// <summary>
        /// Gets or sets the entity reporting interval.
        /// </summary>
        /// <remarks>
        /// This is used to determined how many entities should be processed before reporting status.
        /// </remarks>
        public virtual int EntityReportingInterval
        {
            get
            {
                return m_entityReportingInterval;
            }
            set
            {
                m_entityReportingInterval = value;
            }
        }

        /// <summary>
        /// Gets or sets flag indicating if the adapter has been initialized successfully.
        /// </summary>
        public virtual bool Initialized
        {
            get
            {
                return m_initialized;
            }
            set
            {
                m_initialized = value;
            }
        }

        /// <summary>
        /// Gets or sets enabled state of this <see cref="AdapterBase"/>.
        /// </summary>
        /// <remarks>
        /// Base class simply starts or stops <see cref="AdapterBase"/> based on flag value. Derived classes should
        /// extend this if needed to enable or disable operational state of the adapter based on flag value.
        /// </remarks>
        public bool Enabled
        {
            get
            {
                return m_enabled;
            }
            set
            {
                if (m_enabled && !value)
                    Stop();
                else if (!m_enabled && value)
                    Start();
            }
        }

        /// <summary>
        /// Gets the UTC time this <see cref="AdapterBase"/> was started.
        /// </summary>
        public Ticks StartTime
        {
            get
            {
                return m_startTime;
            }
        }

        /// <summary>
        /// Gets the UTC time this <see cref="AdapterBase"/> was stopped.
        /// </summary>
        public Ticks StopTime
        {
            get
            {
                return m_stopTime;
            }
        }

        /// <summary>
        /// Gets the flag indicating if this adapter supports temporal processing.
        /// </summary>
        public abstract bool SupportsTemporalProcessing
        {
            get;
        }

        /// <summary>
        /// Gets the start time temporal processing constraint defined by call to <see cref="SetTemporalConstraint"/>.
        /// </summary>
        /// <remarks>
        /// This value will be <see cref="DateTime.MinValue"/> when start time constraint is not set - meaning the adapter
        /// is processing data in real-time.
        /// </remarks>
        public virtual DateTime StartTimeConstraint
        {
            get
            {
                return m_startTimeConstraint;
            }
        }

        /// <summary>
        /// Gets the stop time temporal processing constraint defined by call to <see cref="SetTemporalConstraint"/>.
        /// </summary>
        /// <remarks>
        /// This value will be <see cref="DateTime.MaxValue"/> when stop time constraint is not set - meaning the adapter
        /// is processing data in real-time.
        /// </remarks>
        public virtual DateTime StopTimeConstraint
        {
            get
            {
                return m_stopTimeConstraint;
            }
        }

        /// <summary>
        /// Gets or sets the desired processing interval, in milliseconds, for the adapter.
        /// </summary>
        /// <remarks>
        /// With the exception of the values of -1 and 0, this value specifies the desired processing interval for data, i.e.,
        /// basically a delay, or timer interval, over which to process data. A value of -1 means to use the default processing
        /// interval while a value of 0 means to process data as fast as possible.
        /// </remarks>
        public virtual int ProcessingInterval
        {
            get
            {
                return m_processingInterval;
            }
            set
            {
                m_processingInterval = value;

                if (m_processingInterval < -1)
                    m_processingInterval = -1;
            }
        }

        /// <summary>
        /// Gets the total amount of time, in seconds, that the adapter has been active.
        /// </summary>
        public virtual Time RunTime
        {
            get
            {
                Ticks processingTime = 0;

                if (m_startTime > 0)
                {
                    if (m_stopTime > 0)
                        processingTime = m_stopTime - m_startTime;
                    else
                        processingTime = DateTime.UtcNow.Ticks - m_startTime;
                }

                if (processingTime < 0)
                    processingTime = 0;

                return processingTime.ToSeconds();
            }
        }

        /// <summary>
        /// Gets settings <see cref="Dictionary{TKey,TValue}"/> parsed when <see cref="ConnectionString"/> was assigned.
        /// </summary>
        public Dictionary<string, string> Settings
        {
            get
            {
                return m_settings;
            }
        }

        /// <summary>
        /// Gets the settings object used to parse the connection string with the <see cref="ConnectionStringParser"/>.
        /// </summary>
        /// <remarks>
        /// Return null to use the adapter itself as the settings object. This is the default.
        /// </remarks>
        public virtual object SettingsObject
        {
            get
            {
                return null;
            }
        }

        /// <summary>
        /// Gets the status of this <see cref="AdapterBase"/>.
        /// </summary>
        /// <remarks>
        /// Derived classes should provide current status information about the adapter for display purposes.
        /// </remarks>
        public virtual string Status
        {
            get
            {
                const int MaxSignalsToShow = 10;

                StringBuilder status = new StringBuilder();
                DataSet dataSource = DataSource;

                status.AppendFormat("       Data source defined: {0}", ((object)dataSource != null));
                status.AppendLine();

                if ((object)dataSource != null)
                {
                    status.AppendFormat("    Referenced data source: {0}, {1} tables", dataSource.DataSetName, dataSource.Tables.Count);
                    status.AppendLine();
                }

                status.AppendFormat("    Initialization timeout: {0}", InitializationTimeout < 0 ? "Infinite" : InitializationTimeout + " milliseconds");
                status.AppendLine();
                status.AppendFormat("       Adapter initialized: {0}", Initialized);
                status.AppendLine();
                status.AppendFormat("         Operational state: {0}", Enabled ? "Running" : "Stopped");
                status.AppendLine();
                status.AppendFormat("         Connect on demand: {0}", !AutoStart);
                status.AppendLine();
                status.AppendFormat("        Processed entities: {0}", ProcessedEntities);
                status.AppendLine();
                status.AppendFormat("    Total adapter run time: {0}", RunTime);
                status.AppendLine();
                status.AppendFormat("       Temporal processing: {0}", SupportsTemporalProcessing ? "Supported" : "Unsupported");
                status.AppendLine();

                if (SupportsTemporalProcessing)
                {
                    status.AppendFormat("     Start time constraint: {0}", StartTimeConstraint == DateTime.MinValue ? "Unspecified" : StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff"));
                    status.AppendLine();
                    status.AppendFormat("      Stop time constraint: {0}", StopTimeConstraint == DateTime.MaxValue ? "Unspecified" : StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fff"));
                    status.AppendLine();
                    status.AppendFormat("       Processing interval: {0}", ProcessingInterval < 0 ? "Default" : (ProcessingInterval == 0 ? "As fast as possible" : ProcessingInterval + " milliseconds"));
                    status.AppendLine();
                }

                status.AppendFormat("   Item reporting interval: {0}", EntityReportingInterval);
                status.AppendLine();
                status.AppendFormat("                Adapter ID: {0}", ID);
                status.AppendLine();

                Dictionary<string, string> keyValuePairs = Settings;
                char[] keyChars;
                string value;

                status.AppendFormat("         Connection string: {0} key/value pairs", keyValuePairs.Count);
                //                            1         2         3         4         5         6         7
                //                   123456789012345678901234567890123456789012345678901234567890123456789012345678
                //                                         Key = Value
                //                                                        1         2         3         4         5
                //                                               12345678901234567890123456789012345678901234567890
                status.AppendLine();
                status.AppendLine();

                foreach (KeyValuePair<string, string> item in keyValuePairs)
                {
                    keyChars = item.Key.Trim().ToCharArray();
                    keyChars[0] = char.ToUpper(keyChars[0]);

                    value = item.Value.Trim();
                    if (value.Length > 50)
                        value = value.TruncateRight(47) + "...";

                    status.AppendFormat("{0} = {1}", (new string(keyChars)).TruncateRight(25).PadLeft(25), value.PadRight(50));
                    status.AppendLine();
                }

                status.AppendLine();

                if (OutputSignalIDs.Count > 0)
                {
                    status.AppendFormat("            Output signals: {0} defined signals", OutputSignalIDs.Count);
                    status.AppendLine();
                    status.AppendLine();

                    foreach (Guid signalID in OutputSignalIDs.Take(MaxSignalsToShow))
                    {
                        status.Append(this.GetSignalInfo(signalID, maxLength: 40).PadLeft(40));
                        status.Append(" ");
                        status.AppendLine(signalID.ToString());
                    }

                    if (OutputSignalIDs.Count > MaxSignalsToShow)
                        status.AppendLine("...".PadLeft(26));

                    status.AppendLine();
                }

                if (InputSignalIDs.Count > 0)
                {
                    status.AppendFormat("             Input Signals: {0} defined signals", InputSignalIDs.Count);
                    status.AppendLine();
                    status.AppendLine();

                    foreach (Guid signalID in InputSignalIDs.Take(MaxSignalsToShow))
                    {
                        status.Append(this.GetSignalInfo(signalID, maxLength: 40).PadLeft(40));
                        status.Append(" ");
                        status.AppendLine(signalID.ToString());
                    }

                    if (InputSignalIDs.Count > MaxSignalsToShow)
                        status.AppendLine("...".PadLeft(26));

                    status.AppendLine();
                }

                return status.ToString();
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Releases all the resources used by the <see cref="AdapterBase"/> object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="AdapterBase"/> object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                m_disposed = true;  // Prevent duplicate dispose.

                m_inputSignals.CollectionChanged -= m_inputSignals_CollectionChanged;
                m_outputSignals.CollectionChanged -= m_outputSignals_CollectionChanged;

                if (Disposed != null)
                    Disposed(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Initializes <see cref="AdapterBase"/>.
        /// </summary>
        public virtual void Initialize()
        {
            Initialized = false;

            Dictionary<string, string> settings = Settings;
            string setting;

            if (settings.TryGetValue("inputSignals", out setting))
                m_inputSignals.UnionWith(ParseFilterExpression(DataSource, true, setting));

            if (settings.TryGetValue("outputSignals", out setting))
                m_outputSignals.UnionWith(ParseFilterExpression(DataSource, true, setting));

            if (settings.TryGetValue("entityReportingInterval", out setting))
                EntityReportingInterval = int.Parse(setting);
            else
                EntityReportingInterval = DefaultEntityReportingInterval;

            if (settings.TryGetValue("connectOnDemand", out setting))
                AutoStart = !setting.ParseBoolean();
            else
                AutoStart = true;

            string startTime, stopTime, parameters;

            bool startTimeDefined = settings.TryGetValue("startTimeConstraint", out startTime);
            bool stopTimeDefined = settings.TryGetValue("stopTimeConstraint", out stopTime);

            if (startTimeDefined || stopTimeDefined)
            {
                settings.TryGetValue("timeConstraintParameters", out parameters);
                SetTemporalConstraint(startTime, stopTime, parameters);
            }

            int processingInterval;

            if (settings.TryGetValue("processingInterval", out setting) && !string.IsNullOrWhiteSpace(setting) && int.TryParse(setting, out processingInterval))
                ProcessingInterval = processingInterval;
        }

        /// <summary>
        /// Starts the <see cref="AdapterBase"/> or restarts it if it is already running.
        /// </summary>
        [AdapterCommand("Starts the adapter, or restarts if already running.", "Administrator", "Editor")]
        public virtual void Start()
        {
            if (m_disposed)
                throw new ObjectDisposedException(Name, "Cannot start adapter because it is disposed.");

            if (!Initialized)
                throw new InvalidOperationException("Cannot start adapter because it had not been initialized.");

            // Make sure we are stopped (e.g., disconnected) before attempting to start (e.g., connect)
            if (m_enabled)
                Stop();

            m_enabled = true;
            m_stopTime = 0;
            m_startTime = DateTime.UtcNow.Ticks;
        }

        /// <summary>
        /// Stops the <see cref="AdapterBase"/>.
        /// </summary>		
        [AdapterCommand("Stops the adapter.", "Administrator", "Editor")]
        public virtual void Stop()
        {
            if (m_disposed)
                throw new ObjectDisposedException(Name, "Cannot stop adapter because it is disposed.");

            m_enabled = false;
            m_stopTime = DateTime.UtcNow.Ticks;
        }

        /// <summary>
        /// Manually sets the initialized state of the <see cref="AdapterBase"/>.
        /// </summary>
        /// <param name="initialized">Desired initialized state.</param>
        [AdapterCommand("Manually sets the initialized state of the adapter.", "Administrator", "Editor")]
        public virtual void SetInitializedState(bool initialized)
        {
            Initialized = initialized;
        }

        /// <summary>
        /// Gets a short one-line status of this <see cref="AdapterBase"/>.
        /// </summary>
        /// <param name="maxLength">Maximum number of available characters for display.</param>
        /// <returns>A short one-line summary of the current status of this <see cref="AdapterBase"/>.</returns>
        public abstract string GetShortStatus(int maxLength);

        /// <summary>
        /// Defines a temporal processing constraint for the adapter.
        /// </summary>
        /// <param name="startTime">Defines a relative or exact start time for the temporal constraint.</param>
        /// <param name="stopTime">Defines a relative or exact stop time for the temporal constraint.</param>
        /// <param name="constraintParameters">Defines any temporal parameters related to the constraint.</param>
        /// <remarks>
        /// <para>
        /// This method defines a temporal processing constraint for an adapter, i.e., the start and stop time over which an
        /// adapter will process data. Actual implementation of the constraint will be adapter specific. Implementations
        /// should be able to dynamically handle multiple calls to this function with new constraints. Passing in <c>null</c>
        /// for the <paramref name="startTime"/> and <paramref name="stopTime"/> should cancel the temporal constraint and
        /// return the adapter to standard / real-time operation.
        /// </para>
        /// <para>
        /// The <paramref name="startTime"/> and <paramref name="stopTime"/> parameters can be specified in one of the
        /// following formats:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Time Format Example</term>
        ///         <description>Format Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term>12-30-2000 23:59:59.033</term>
        ///         <description>Absolute date and time.</description>
        ///     </item>
        ///     <item>
        ///         <term>*</term>
        ///         <description>Evaluates to <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-20s</term>
        ///         <description>Evaluates to 20 seconds before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-10m</term>
        ///         <description>Evaluates to 10 minutes before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-1h</term>
        ///         <description>Evaluates to 1 hour before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-1d</term>
        ///         <description>Evaluates to 1 day before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        /// </list>
        /// </para>
        /// </remarks>
        [AdapterCommand("Defines a temporal processing constraint for the adapter.", "Administrator", "Editor", "Viewer")]
        public virtual void SetTemporalConstraint(string startTime, string stopTime, string constraintParameters)
        {
            if (!string.IsNullOrWhiteSpace(startTime))
                m_startTimeConstraint = ParseTimeTag(startTime);
            else
                m_startTimeConstraint = DateTime.MinValue;

            if (!string.IsNullOrWhiteSpace(stopTime))
                m_stopTimeConstraint = ParseTimeTag(stopTime);
            else
                m_stopTimeConstraint = DateTime.MaxValue;
        }

        /// <summary>
        /// Raises the <see cref="StatusMessage"/> event.
        /// </summary>
        /// <param name="status">New status message.</param>
        protected virtual void OnStatusMessage(string status)
        {
            try
            {
                if ((object)StatusMessage != null)
                    StatusMessage(this, new EventArgs<string>(status));
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for StatusMessage event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises the <see cref="StatusMessage"/> event with a formatted status message.
        /// </summary>
        /// <param name="formattedStatus">Formatted status message.</param>
        /// <param name="args">Arguments for <paramref name="formattedStatus"/>.</param>
        /// <remarks>
        /// This overload combines string.Format and SendStatusMessage for convenience.
        /// </remarks>
        protected virtual void OnStatusMessage(string formattedStatus, params object[] args)
        {
            try
            {
                if ((object)StatusMessage != null)
                    StatusMessage(this, new EventArgs<string>(string.Format(formattedStatus, args)));
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for StatusMessage event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises <see cref="ProcessException"/> event.
        /// </summary>
        /// <param name="ex">Processing <see cref="Exception"/>.</param>
        protected virtual void OnProcessException(Exception ex)
        {
            if ((object)ProcessException != null)
                ProcessException(this, new EventArgs<Exception>(ex));
        }

        /// <summary>
        /// Raises <see cref="InputSignalIDsUpdated"/> event.
        /// </summary>
        protected virtual void OnInputSignalsUpdated()
        {
            try
            {
                if ((object)InputSignalIDsUpdated != null)
                    InputSignalIDsUpdated(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for InputSignalsUpdated event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises <see cref="OutputSignalIDsUpdated"/> event.
        /// </summary>
        protected virtual void OnOutputSignalsUpdated()
        {
            try
            {
                if ((object)OutputSignalIDsUpdated != null)
                    OutputSignalIDsUpdated(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for OutputSignalsUpdated event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises <see cref="ConfigurationChanged"/> event.
        /// </summary>
        protected virtual void OnConfigurationChanged()
        {
            try
            {
                if ((object)ConfigurationChanged != null)
                    ConfigurationChanged(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for ConfigurationChanged event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Raises the <see cref="EntitiesDiscarded"/> event.
        /// </summary>
        /// <param name="entities">Enumeration of <see cref="ITimeSeriesEntity"/> objects being discarded.</param>
        protected virtual void OnEntitiesDiscarded(IEnumerable<ITimeSeriesEntity> entities)
        {
            try
            {
                if ((object)EntitiesDiscarded != null)
                    EntitiesDiscarded(this, new EventArgs<IEnumerable<ITimeSeriesEntity>>(entities));
            }
            catch (Exception ex)
            {
                // We protect our code from consumer thrown exceptions
                OnProcessException(new InvalidOperationException(string.Format("Exception in consumer handler for DiscardingMeasurements event: {0}", ex.Message), ex));
            }
        }

        /// <summary>
        /// Safely increments the total processed time-series entities.
        /// </summary>
        /// <param name="totalAdded">The number of time-series entities processed.</param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        protected virtual void IncrementProcessedEntities(long totalAdded)
        {
            // Check to see if total number of added points will exceed process interval used to show periodic
            // messages of how many points have been archived so far...
            int interval = EntityReportingInterval;

            if (interval > 0)
            {
                bool showMessage = m_processedEntities + totalAdded >= (m_processedEntities / interval + 1) * interval;

                m_processedEntities += totalAdded;

                if (showMessage)
                    OnStatusMessage("{0:N0} entities have been processed so far...", m_processedEntities);
            }
        }

        private void m_inputSignals_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnInputSignalsUpdated();
        }

        private void m_outputSignals_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnOutputSignalsUpdated();
        }

        #endregion

        #region [ Static ]

        // Static Fields
        private static readonly Regex s_filterExpression = new Regex("(FILTER[ ]+(TOP[ ]+(?<MaxRows>\\d+)[ ]+)?(?<TableName>\\w+)[ ]+WHERE[ ]+(?<Expression>.+)[ ]+ORDER[ ]+BY[ ]+(?<SortField>\\w+))|(FILTER[ ]+(TOP[ ]+(?<MaxRows>\\d+)[ ]+)?(?<TableName>\\w+)[ ]+WHERE[ ]+(?<Expression>.+))", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly Regex s_timetagExpression = new Regex("\\*(?<Offset>[+-]?\\d*\\.?\\d*)(?<Unit>\\w+)", RegexOptions.Compiled);
        private static readonly ConcurrentDictionary<Guid, DataRow> s_metaDataCache = new ConcurrentDictionary<Guid, DataRow>();

        // Static Methods

        /// <summary>
        /// Parses a standard FILTER style expression into its constituent parts.
        /// </summary>
        /// <param name="filterExpression">Filter expression to parse.</param>
        /// <param name="tableName">Name of table in filter expression.</param>
        /// <param name="whereExpression">Where expression in filter expression.</param>
        /// <param name="sortField">Sort field, if any, in filter expression.</param>
        /// <param name="takeCount">Total row restriction, if any, in filter expression.</param>
        /// <returns><c>true</c> if filter expression was successfully parsed; otherwise <c>false</c>.</returns>
        /// <remarks>
        /// This method can be safely called from multiple threads.
        /// </remarks>
        public static bool ParseFilterSyntax(string filterExpression, out string tableName, out string whereExpression, out string sortField, out int takeCount)
        {
            tableName = null;
            whereExpression = null;
            sortField = null;
            takeCount = 0;

            if (string.IsNullOrWhiteSpace(filterExpression))
                return false;

            Match filterMatch;

            lock (s_filterExpression)
            {
                filterMatch = s_filterExpression.Match(filterExpression.ReplaceControlCharacters());
            }

            if (!filterMatch.Success)
                return false;

            tableName = filterMatch.Result("${TableName}").Trim();
            whereExpression = filterMatch.Result("${Expression}").Trim();
            sortField = filterMatch.Result("${SortField}").Trim();
            string maxRows = filterMatch.Result("${MaxRows}").Trim();

            if (string.IsNullOrEmpty(maxRows) || !int.TryParse(maxRows, out takeCount))
                takeCount = int.MaxValue;

            return true;
        }

        /// <summary>
        /// Parses a string formatted as an absolute or relative time tag.
        /// </summary>
        /// <param name="timetag">String formatted as an absolute or relative time tag.</param>
        /// <returns>
        /// <see cref="DateTime"/> representing the parsed <paramref name="timetag"/> string formatted as an absolute or relative time tag.
        /// </returns>
        /// <remarks>
        /// <para>
        /// Relative times are parsed based on an offset to current time (UTC) specified by "*".
        /// </para>
        /// <para>
        /// The <paramref name="timetag"/> parameter can be specified in one of the following formats:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Time Format</term>
        ///         <description>Format Description</description>
        ///     </listheader>
        ///     <item>
        ///         <term>12-30-2000 23:59:59.033</term>
        ///         <description>Absolute date and time.</description>
        ///     </item>
        ///     <item>
        ///         <term>*</term>
        ///         <description>Evaluates to <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-20s</term>
        ///         <description>Evaluates to 20 seconds before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-10m</term>
        ///         <description>Evaluates to 10 minutes before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-1h</term>
        ///         <description>Evaluates to 1 hour before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        ///     <item>
        ///         <term>*-1d</term>
        ///         <description>Evaluates to 1 day before <see cref="DateTime.UtcNow"/>.</description>
        ///     </item>
        /// </list>
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="timetag"/> parameter cannot be null or empty.</exception>
        /// <exception cref="FormatException"><paramref name="timetag"/> does not contain a valid string representation of a date and time.</exception>
        public static DateTime ParseTimeTag(string timetag)
        {
            if (string.IsNullOrWhiteSpace(timetag))
                throw new ArgumentNullException("timetag", "Timetag string cannot be null or empty.");

            DateTime dateTime;

            if (timetag.Contains("*"))
            {
                // Relative time is specified.
                // Examples:
                // 1) * (Now)
                // 2) *-20s (20 seconds ago)
                // 3) *-10m (10 minutes ago)
                // 4) *-1h (1 hour ago)
                // 5) *-1d (1 day ago)

                dateTime = DateTime.UtcNow;
                timetag = timetag.RemoveWhiteSpace();

                if (timetag.Length > 1)
                {
                    Match timetagMatch;

                    lock (s_timetagExpression)
                    {
                        timetagMatch = s_timetagExpression.Match(timetag);
                    }

                    if (timetagMatch.Success)
                    {
                        double offset = double.Parse(timetagMatch.Result("${Offset}").Trim());
                        string unit = timetagMatch.Result("${Unit}").Trim().ToLower();

                        switch (unit[0])
                        {
                            case 's':
                                dateTime = dateTime.AddSeconds(offset);
                                break;
                            case 'm':
                                dateTime = dateTime.AddMinutes(offset);
                                break;
                            case 'h':
                                dateTime = dateTime.AddHours(offset);
                                break;
                            case 'd':
                                dateTime = dateTime.AddDays(offset);
                                break;
                        }
                    }
                    else
                    {
                        // Expression match failed, attempt to parse absolute time specification.
                        dateTime = DateTime.Parse(timetag, CultureInfo.InvariantCulture);
                    }
                }
            }
            else
            {
                // Absolute time is specified.
                dateTime = DateTime.Parse(timetag, CultureInfo.InvariantCulture);
            }

            return dateTime;
        }

        // TODO: Filter expressions would be better able to accommodate sourceIDs if a "source" was available as independent field in active measurements (i.e., instead of part of ID - for that matter, so would point ID - leave point ID?, but definitely add its fields independently)

        /// <summary>
        /// Loads an <see cref="IOutputAdapter"/> or <see cref="IActionAdapter"/> instance's input signals from a specific set of source ID's.
        /// </summary>
        /// <param name="adapter"><see cref="IAdapter"/> to load input signals for.</param>
        /// <param name="measurementTable">Measurement table name used to load input source ID's.</param>
        /// <remarks>
        /// Any existing input signals will be distinctly merged with those associated with specified source ID's.
        /// </remarks>
        public static void LoadInputSourceIDs(IAdapter adapter, string measurementTable = "ActiveMeasurements")
        {
            string[] sourceIDs = null;

            if (adapter is IOutputAdapter)
                sourceIDs = (adapter as IOutputAdapter).InputSourceIDs;
            else if (adapter is IActionAdapter)
                sourceIDs = (adapter as IActionAdapter).InputSourceIDs;

            if (sourceIDs != null)
            {
                // Attempt to lookup input measurement keys for given source IDs from default measurement table, if defined
                try
                {
                    if (adapter.DataSource.Tables.Contains(measurementTable))
                    {
                        StringBuilder likeExpression = new StringBuilder();

                        // Build like expression for each source ID
                        foreach (string sourceID in sourceIDs)
                        {
                            if (likeExpression.Length > 0)
                                likeExpression.Append(" OR ");

                            likeExpression.AppendFormat("ID LIKE '{0}:*'", sourceID);
                        }

                        DataRow[] filteredRows = adapter.DataSource.Tables[measurementTable].Select(likeExpression.ToString());

                        if (filteredRows.Length > 0)
                            adapter.InputSignalIDs.UnionWith(filteredRows.Select(row => row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>()));
                    }
                }
                catch
                {
                    // Errors here are not catastrophic, this simply limits the auto-assignment of input signals based on specified source ID's
                }
            }
        }

        /// <summary>
        /// Loads an <see cref="IInputAdapter"/> or <see cref="IActionAdapter"/> instance's output signals from a specific set of source ID's.
        /// </summary>
        /// <param name="adapter"><see cref="IAdapter"/> to load output signals for.</param>
        /// <param name="measurementTable">Measurement table name used to load output source ID's.</param>
        /// <remarks>
        /// Any existing output signals will be distinctly merged with those associated with specified source ID's.
        /// </remarks>
        public static void LoadOutputSourceIDs(IAdapter adapter, string measurementTable = "ActiveMeasurements")
        {
            string[] sourceIDs = null;

            if (adapter is IInputAdapter)
                sourceIDs = (adapter as IInputAdapter).OutputSourceIDs;
            else if (adapter is IActionAdapter)
                sourceIDs = (adapter as IActionAdapter).OutputSourceIDs;

            if (sourceIDs != null)
            {
                // Attempt to lookup output measurement keys for given source IDs from default measurement table, if defined
                try
                {
                    if (adapter.DataSource.Tables.Contains(measurementTable))
                    {
                        StringBuilder likeExpression = new StringBuilder();

                        // Build like expression for each source ID
                        foreach (string sourceID in sourceIDs)
                        {
                            if (likeExpression.Length > 0)
                                likeExpression.Append(" OR ");

                            likeExpression.AppendFormat("ID LIKE '{0}:*'", sourceID);
                        }

                        DataRow[] filteredRows = adapter.DataSource.Tables[measurementTable].Select(likeExpression.ToString());

                        if (filteredRows.Length > 0)
                            adapter.OutputSignalIDs.UnionWith(filteredRows.Select(row => row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>()));
                    }
                }
                catch
                {
                    // Errors here are not catastrophic, this simply limits the auto-assignment of output signals based on specified source ID's
                }
            }
        }

        // Filter expression can use DataSource for filtering desired set of input or output
        // signals based on any table and fields in the data set instead of a simple a list
        // of signal ID's. The format is as follows:

        //  FILTER [TOP <MaxRows>] <TableName> WHERE <Expression> [ORDER BY <SortField>]

        // Source tables are expected to have at least the following fields:
        //
        //      ID          NVARCHAR    Measurement key formatted as: ArchiveSource:PointID
        //      SignalID    GUID        Unique identification for signal
        //      PointTag    NVARCHAR    Point tag of signal
        //      Adder       FLOAT       Adder to apply to value, if any (default to 0.0)
        //      Multiplier  FLOAT       Multiplier to apply to value, if any (default to 1.0)
        //
        // Could have used standard SQL syntax here but didn't want to give user the impression
        // that this a standard SQL expression when it isn't - so chose the word FILTER to make
        // consumer was aware that this was not SQL, but SQL "like". The WHERE clause expression
        // uses standard SQL syntax (it is simply the DataTable.Select filter expression).

        /// <summary>
        /// Parses filter expression from connection string setting.
        /// </summary>
        /// <param name="dataSource">The <see cref="DataSet"/> used to define data that can be used in the expression.</param>
        /// <param name="allowSelect">Determines if database access via "SELECT" statement should be allowed for defining input signals (see remarks about security).</param>
        /// <param name="value">Value of setting used to define set of signals.</param>
        /// <param name="measurementTable">Measurement table name used to load additional meta-data; this is not used when specifying FILTER syntax.</param>
        /// <returns>Set of signals defined by <paramref name="value"/>.</returns>
        /// <remarks>
        /// Security warning: allowing SELECT statements, i.e., setting <paramref name="allowSelect"/> to <c>true</c>, should only be allowed in cases where SQL
        /// injection would not be an issue (e.g., in places where a user can already access the database and would have nothing to gain via an injection attack).
        /// </remarks>
        public static ISet<Guid> ParseFilterExpression(DataSet dataSource, bool allowSelect, string value, string measurementTable = "ActiveMeasurements")
        {
            HashSet<Guid> signalIDs = new HashSet<Guid>();
            bool dataSourceAvailable = ((object)dataSource != null);

            string tableName, expression, sortField;
            int takeCount;

            MeasurementKey key;
            Guid id;

            value = value.Trim();

            if (string.IsNullOrWhiteSpace(value))
                return signalIDs;

            if (dataSourceAvailable && ParseFilterSyntax(value, out tableName, out expression, out sortField, out takeCount))
            {
                // Filter expression was defined using FILTER syntax--run the query against the data set
                foreach (DataRow row in dataSource.Tables[tableName].Select(expression, sortField).Take(takeCount))
                    signalIDs.Add(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>());
            }
            else if (allowSelect && value.StartsWith("SELECT ", StringComparison.InvariantCultureIgnoreCase))
            {
                try
                {
                    // Filter expression was defined using SELECT syntax--check global
                    // security settings and run the query against the database

                    // Load security setting from the system settings category
                    ConfigurationFile config = ConfigurationFile.Current;
                    CategorizedSettingsElementCollection settings = config.Settings["systemSettings"];
                    settings.Add("AllowSelectFilterExpresssions", false, "Determines if database backed SELECT statements should be allowed as filter expressions for defining input and output signals for adapters.");

                    // Global configuration setting can override ability to use SELECT statements
                    if (settings["AllowSelectFilterExpresssions"].ValueAsBoolean())
                    {
                        using (AdoDataConnection database = new AdoDataConnection("systemSettings"))
                        {
                            DataTable results = database.Connection.RetrieveData(database.AdapterType, value);

                            foreach (DataRow row in results.Rows)
                                signalIDs.Add(row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>());
                        }
                    }
                    else
                    {
                        throw new InvalidOperationException("Database backed SELECT statements are disabled in the configuration.");
                    }
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException(string.Format("Could not parse signal definition from select statement \"{0}\": {1}", value, ex.Message), ex);
                }
            }
            else
            {
                // Filter expression was defined as a simple list
                // of signals--parse each signal individually
                foreach (string item in value.Split(';'))
                {
                    if (string.IsNullOrWhiteSpace(item))
                        continue;

                    // Attempt to parse it as a GUID
                    if (!Guid.TryParse(item, out id))
                    {
                        // Attempt to parse it as a measurement key
                        if (MeasurementKey.TryParse(item, out key))
                        {
                            // Search the data source for measurement keys that match the parsed measurement key
                            if (dataSourceAvailable && dataSource.Tables.Contains(measurementTable))
                            {
                                DataRow[] filteredRows = dataSource.Tables[measurementTable].Select(string.Format("ID = '{0}'", key.ToString()));

                                if (filteredRows.Length > 0)
                                    id = filteredRows[0]["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>();
                            }
                        }
                        else
                        {
                            // It couldn't be parsed as a measurement key, so assume the value is a point tag instead.
                            // Search the data source for point tags that match the given point tag
                            if (dataSourceAvailable && dataSource.Tables.Contains(measurementTable))
                            {
                                DataRow[] filteredRows = dataSource.Tables[measurementTable].Select(string.Format("PointTag = '{0}'", item));

                                if (filteredRows.Length > 0)
                                    id = filteredRows[0]["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType<Guid>();
                            }
                        }
                    }

                    // If we were able to find a valid signal ID, add it to the list of signals
                    if (id != Guid.Empty)
                        signalIDs.Add(id);
                    else
                        throw new InvalidOperationException(string.Format("Could not parse input signal definition \"{0}\" as a filter expression, measurement key, point tag or Guid", item));
                }
            }

            return signalIDs;
        }

        /// <summary>
        /// Looks up the <see cref="MeasurementKey"/> for the specified <paramref name="signalID"/> by searching in the <paramref name="dataSource"/>.
        /// </summary>
        /// <param name="dataSource"><see cref="DataSet"/> containing meta-data to be searched for the <see cref="MeasurementKey"/>.</param>
        /// <param name="signalID">The <see cref="Guid"/> for the signal to look up the <see cref="MeasurementKey"/>.</param>
        /// <param name="measurementTable">Measurement table name to search for <see cref="MeasurementKey"/>.</param>
        /// <param name="measurementKeyColumn">Name of column that contains data to parse as a <see cref="MeasurementKey"/>.</param>
        /// <returns>The <see cref="MeasurementKey"/> for the given <paramref name="signalID"/> or an empty <see cref="MeasurementKey"/> if record cannot be found.</returns>
        public static MeasurementKey LookUpMeasurementKey(DataSet dataSource, Guid signalID, string measurementTable = "ActiveMeasurements", string measurementKeyColumn = "ID")
        {
            DataRow row;

            if (TryGetMetadata(dataSource, signalID, out row, measurementTable))
                return row.GetMeasurementKey(measurementKeyColumn);

            return default(MeasurementKey);
        }

        /// <summary>
        /// Attempts to parse an individual signal ID from the specified <paramref name="parameterName"/> out of the <see cref="ConnectionString"/>.
        /// </summary>
        /// <param name="dataSource">The <see cref="DataSet"/>, if any, used to define data that can be used for the filter expression found in <paramref name="parameterName"/>.</param>
        /// <param name="settings">Connection string settings dictionary.</param>
        /// <param name="parameterName">Parameter name for the signal ID expected in the <see cref="ConnectionString"/>.</param>
        /// <param name="signalID">The returned <see cref="Guid"/> based signal ID if successfully parsed.</param>
        /// <returns><c>true</c> if signal ID was successfully parsed; otherwise, <c>false</c>.</returns>
        /// <remarks>
        /// <para>
        /// This function is useful when input or output signals need to be individually specified, e.g., in a calculation, where the signals are
        /// identified as connection string parameters.
        /// </para>
        /// <para>
        /// If <paramref name="parameterName"/> value is a filter expression that returns more than one value, first value is returned.
        /// </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="settings"/> is <c>null</c>.</exception>
        public static bool TryParseSignalID(DataSet dataSource, Dictionary<string, string> settings, string parameterName, out Guid signalID)
        {
            Guid[] signalIDs;

            if (TryParseSignalIDs(dataSource, settings, parameterName, out signalIDs))
            {
                signalID = signalIDs[0];
                return true;
            }

            signalID = Guid.Empty;
            return false;
        }

        /// <summary>
        /// Attempts to parse signal IDs from the specified <paramref name="parameterName"/> out of the <see cref="ConnectionString"/>.
        /// </summary>
        /// <param name="dataSource">The <see cref="DataSet"/>, if any, used to define data that can be used for the filter expression found in <paramref name="parameterName"/>.</param>
        /// <param name="settings">Connection string settings dictionary.</param>
        /// <param name="parameterName">Parameter name for the signal ID expected in the <see cref="ConnectionString"/>.</param>
        /// <param name="signalIDs">The returned <see cref="Guid"/> based signal IDs if successfully parsed.</param>
        /// <returns><c>true</c> if any signal IDs were successfully parsed; otherwise, <c>false</c>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="settings"/> is <c>null</c>.</exception>
        public static bool TryParseSignalIDs(DataSet dataSource, Dictionary<string, string> settings, string parameterName, out Guid[] signalIDs)
        {
            if ((object)settings == null)
                throw new ArgumentNullException("settings");

            string parameterValue;

            if (settings.TryGetValue(parameterName, out parameterValue) && !string.IsNullOrWhiteSpace(parameterValue))
            {
                signalIDs = ParseFilterExpression(dataSource, true, parameterValue).ToArray();

                if (signalIDs.Length > 0)
                    return true;
            }

            signalIDs = null;
            return false;
        }

        /// <summary>
        /// Gets a formatted string representing the <paramref name="signalID"/> in human identifiable form.
        /// </summary>
        /// <param name="dataSource"><see cref="DataSet"/> containing meta-data to be searched.</param>
        /// <param name="signalID">The <see cref="Guid"/> for the signal to look up the meta-data.</param>
        /// <param name="measurementTable">Measurement table name to search for meta-data.</param>
        /// <param name="measurementKeyColumn">Name of column that contains the data to parse as a <see cref="MeasurementKey"/>.</param>
        /// <param name="signalTypeColumn">Name of column that contains the data to parse as a <see cref="SignalType"/>.</param>
        /// <param name="pointTagColumn">Name of column that contains the point tag name.</param>
        /// <param name="maxLength">Defines a maximum length for the returned signal information; set to -1 for no limit. Minimum length limit is 20; smaller lengths will be set to 20.</param>
        /// <returns>
        /// A formatted string representing the <paramref name="signalID"/> in human identifiable form.
        /// </returns>
        /// <remarks>
        /// If no meta-data can be found for the specified <paramref name="signalID"/>, the Guid will be serialized as a string and this will be the returned as the signal information.
        /// When the <paramref name="signalID"/> is returned as the signal information, its length will always be 36 characters regardless of <pararef name="maxLength"/> value.
        /// </remarks>
        public static string GetSignalInfo(DataSet dataSource, Guid signalID, string measurementTable = "ActiveMeasurements", string measurementKeyColumn = "ID", string signalTypeColumn = "SignalType", string pointTagColumn = "PointTag", int maxLength = -1)
        {
            if (signalID == Guid.Empty)
                return "Undefined";

            DataRow row;

            if (TryGetMetadata(dataSource, signalID, out row, measurementTable))
            {
                string key = row.GetMeasurementKey(measurementKeyColumn).ToString();
                string pointTag = row[pointTagColumn].ToNonNullNorWhiteSpace(signalID.ToString());
                string type = row.GetSignalType(signalTypeColumn).ToString();

                // Perform length control on signal information display if requested
                if (maxLength != -1)
                {
                    // Set minimum practical limit
                    if (maxLength < 20)
                        maxLength = 20;

                    int baseLength = 6 + key.Length + type.Length;

                    // If key and type alone are already greater than desired length, reduce
                    // signal information display just to the measurement key
                    if (baseLength > maxLength)
                    {
                        // If key alone is already greater than desired length, trim the
                        // measurement key with ellipsis
                        if (2 + key.Length > maxLength)
                            return string.Format("[{0}]", key.TrimWithEllipsisEnd(maxLength - 2));

                        return string.Format("[{0}]", key);
                    }

                    // If key and type alone do not leave enough room for an effective point tag display,
                    // reduce signal information display just to the measurement key and type
                    if (maxLength - baseLength < 8)
                        return string.Format("[{0}] ({1})", key, type);

                    // Perform normal display but limit maximum point tag length
                    return string.Format("[{0}] {1} ({2})", key, pointTag.TrimWithEllipsisEnd(maxLength - baseLength), type);
                }

                // Return full-length signal information display
                return string.Format("[{0}] {1} ({2})", key, pointTag, type);
            }

            return signalID.ToString();
        }

        /// <summary>
        /// Attempts to lookup meta-data associated with the specified <paramref name="signalID"/>.
        /// </summary>
        /// <param name="dataSource"><see cref="DataSet"/> containing meta-data to be searched.</param>
        /// <param name="signalID">The <see cref="Guid"/> for the signal to look up the meta-data.</param>
        /// <param name="row"><see cref="DataRow"/> of meta-data associated with the specified <paramref name="signalID"/>.</param>
        /// <param name="measurementTable">Measurement table name to search for meta-data.</param>
        /// <returns><c>true</c> if meta-data record for <paramref name="signalID"/> was found; otherwise, <c>false</c>.</returns>
        public static bool TryGetMetadata(DataSet dataSource, Guid signalID, out DataRow row, string measurementTable = "ActiveMeasurements")
        {
            row = s_metaDataCache.GetOrAdd(signalID, id =>
            {
                try
                {
                    DataRow[] filteredRows = dataSource.Tables[measurementTable].Select(string.Format("SignalID = '{0}'", id));

                    if (filteredRows.Length > 0)
                        return filteredRows[0];
                }
                catch
                {
                    // Any errors will result in a null data row
                }

                return null;
            });

            return ((object)row != null);
        }

        // Clear meta-data cache
        internal static void ClearMetaDataCache()
        {
            s_metaDataCache.Clear();
        }

        #endregion
    }
}